"""
数据提取-业务流程接口测试
"""

import json
import re
import traceback
from json.decoder import JSONDecodeError

import allure
import jsonpath

from config.setting import FILE_PATH
from utils.assertion.assertions import Assertions
from utils.debugtalk import DebugTalk
from utils.logging_tool.recordlog import logs
from utils.mysql_tool.connect_mysql import ConnectMysql
from utils.read_files_tools.read_extract_yaml import ReadYamlData
from utils.requests_tools.send_request import SendRequest

assert_res = Assertions()


class RequestBase(object):
    def __init__(self):
        self.run = SendRequest()
        self.read = ReadYamlData()
        self.asserts = Assertions()

    def handler_yaml_list(self, data_dict):
        """处理yaml文件测试用例请求参数为list情况，以数组形式"""
        try:
            for key, value in data_dict.items():
                if isinstance(value, list):
                    value_lst = ','.join(value).split(',')
                    data_dict[key] = value_lst
                return data_dict
        except Exception:
            logs.error(str(traceback.format_exc()))

    def replace_load(self, data):
        """yaml数据替换解析"""
        str_data = data
        if not isinstance(data, str):
            str_data = json.dumps(data, ensure_ascii=False)
        for i in range(str_data.count('${')):
            if '${' in str_data and '}' in str_data:

                # index检测字符串是否子字符串，并找到字符串的索引位置
                start_index = str_data.index('$')
                end_index = str_data.index('}', start_index)

                # yaml文件的参数，如：${get_yaml_data(loginname)}
                ref_all_params = str_data[start_index:end_index + 1]

                # 函数名，获取Debugtalk的方法
                func_name = ref_all_params[2:ref_all_params.index("(")]

                # 函数里的参数
                func_params = ref_all_params[ref_all_params.index("(") + 1:ref_all_params.index(")")]

                # 传入替换的参数获取对应的值,*func_params按,分割重新得到一个字符串
                extract_data = getattr(DebugTalk(), func_name)(*func_params.split(',') if func_params else "")

                if extract_data and isinstance(extract_data, list):
                    extract_data = ','.join(e for e in extract_data)
                str_data = str_data.replace(ref_all_params, str(extract_data))

        # 还原数据
        if data and isinstance(data, dict):
            data = json.loads(str_data)
            self.handler_yaml_list(data)
        else:
            data = str_data
        return data

    def specification_yaml(self, case_info):
        """
        规范yaml测试用例的写法
        :param case_info: list类型,调试取case_info[0]-->dict
        :return:
        """

        try:
            # ----------------------------------------处理baseInfo里面的数据----------------------------------------

            # 模块名称
            feature = case_info["baseInfo"]['feature']
            allure.attach(feature, f'模块名称：{feature}')

            # 接口名称
            api_name = case_info["baseInfo"]["api_name"]
            allure.attach(api_name, f'接口名称：{api_name}')

            # 接口地址
            url = DebugTalk().get_baseurl() + case_info["baseInfo"]["url"]
            allure.attach(url, f'接口地址：{url}')

            # 请求方法
            method = case_info["baseInfo"]["method"]
            allure.attach(method, f'请求方法：{method}')

            # 请求头
            headers = case_info["baseInfo"].get('headers', None)
            if headers is not None:
                # yaml 文件中是 ${get_headers(data)} 提取后是字符串格式， eval 字符串转成字典格式；
                headers = eval(self.replace_load(case_info["baseInfo"]["headers"])) if isinstance(headers,
                                                                                                  str) else headers
            allure.attach(str(headers), f'请求头：{headers}', allure.attachment_type.JSON)

            # cookie-【使用session用不到】
            cookies = case_info["baseInfo"].get('cookies', None)
            if cookies is not None:
                cookies = eval(self.replace_load(case_info["baseInfo"]['cookies'])) if isinstance(cookies,
                                                                                                  str) else cookies

            # ----------------------------------------处理testCase里面的数据----------------------------------------

            for testcase in case_info["testCase"]:
                # 测试用例名称
                case_name = testcase.pop("case_name")
                allure.attach(case_name, f'测试用例名称：{case_name}')

                # 用例等级
                rank = testcase.pop('rank', None)
                allure.attach(rank, f'严重等级：{rank}')

                # 下载文件类型
                download_type = testcase.pop("download_type", None)

                # 用例前置条件
                if 'setup' in testcase:
                    # 获取前置方式
                    if 'type' or 'database' or 'sqls' != None:
                        testcase['setup'].pop('type', None)
                        testcase['setup'].pop('database', None)
                        testcase['setup'].pop('sqls', None)
                testcase.pop('setup', None)

                # 用例后置条件
                if 'teardown' in testcase:
                    # 获取后置方式
                    if 'type' or 'database' or 'sqls' != None:
                        testcase['teardown'].pop('type', None)
                        testcase['teardown'].pop('database', None)
                        testcase['teardown'].pop('sqls', None)
                testcase.pop('teardown', None)

                # 断言
                val = self.replace_load(testcase.get('validation'))
                testcase['validation'] = val
                validation = eval(testcase.pop('validation', None))  # 字符串形式的列表转换为list类型
                allure_validation = str([str(list(i.values())) for i in validation])
                allure.attach(allure_validation, "预期结果", allure.attachment_type.JSON)

                # 参数提取
                extract = testcase.pop('extract', None)
                extract_lst = testcase.pop('extract_list', None)

                # 接口的请求参数
                params_type = ['params', 'data', 'json']  # 参数类型
                for key, value in testcase.items():
                    if key in params_type:
                        result = self.replace_load(value)
                        testcase[key] = result

                # 处理文件上传接口
                files, file = testcase.pop('files', None), None
                if files is not None:
                    print(files)
                    for fk, fv in files.items():
                        allure.attach(json.dumps(files), '导入文件')
                        files = {fk: open(fv, mode='rb')}

                res = self.run.run_main(feature=feature,
                                        name=api_name,
                                        url=url,
                                        case_name=case_name,
                                        headers=headers,
                                        cookies=cookies,
                                        method=method,
                                        files=files,
                                        **testcase)

                # 处理下载文件类型的接口请求
                if download_type is not None:
                    with open(f"{FILE_PATH['DOWNLOAD']}{case_name}{download_type}", "wb") as f:
                        f.write(res.content)

                try:
                    status_code = res.status_code
                    logs.info(f'接口响应状态码：{status_code}')
                    allure.attach(str(status_code), f"接口响应状态码:{str(status_code)}")

                    # 判断接口响应信息是否为json格式
                    is_json_pattern = r'^\s*[\{\[].*[\}\]]\s*$'
                    if re.match(is_json_pattern, res.text) is None:

                        logs.info("接口响应信息非json格式，不支持接口信息提取，断言只支持状态码断言")

                        # 处理状态码断言
                        assert_res.assert_result(validation, None, status_code)

                    else:
                        # 接口响应信息json格式，text文本格式需要转换为字典
                        res_json = json.loads(res.text)

                        # 根据接口信息进行提取
                        if extract is not None:
                            self.extract_data(extract, res.text)
                        if extract_lst is not None:
                            self.extract_data_list(extract_lst, res.text)

                        # 处理断言
                        assert_res.assert_result(validation, res_json, status_code)

                        allure.attach(self.allure_attach_response(res.json()), '接口响应信息', allure.attachment_type.JSON)

                except JSONDecodeError as js:
                    logs.error("系统异常或接口未请求！")
                    raise js
                except Exception as e:
                    logs.error(str(traceback.format_exc()))
                    raise e
        except Exception as e:
            logs.error(e)
            raise e

    @classmethod
    def allure_attach_response(cls, response):
        if isinstance(response, dict):
            allure_response = json.dumps(response, ensure_ascii=False, indent=4)
        else:
            allure_response = response
        return allure_response

    @allure.step("测试用例前置")
    def testcase_setup(self, testcases, index):
        """
        测试用例前置操作,从yaml 文件中获取前置方式
        :param testcases:获取到的所有用例
        :param index:
        :return:
        """
        if len(testcases) <= index:
            return
        i = 0
        for i in range(index, index + 1):
            testcase = testcases[i]
            casename = testcase.get('testCase')[0].get('case_name')
            logs.info(f'【用例标题】{casename}')
            logs.info(f'【用例信息】{testcase}')
            # 判断testcase 是否为字典格式，流程中包含多个接口步骤为字典
            if isinstance(testcase, dict):
                try:
                    for key, value in testcase.items():
                        if key == 'testCase':
                            testcase = value
                            if 'setup' in testcase[0]:  # 前置操作
                                # 获取前置方式
                                type = testcase[0]['setup'].get('type', None)
                                logs.info(f'前置方式:{type}')
                                if type == 'db':  # 数据库操作
                                    database = testcase[0]['setup'].get('database', None)
                                    sqls = testcase[0]['setup'].get('sqls', None)
                                    logs.info(f'database:{database}')
                                    logs.info(f'sqls:{sqls}')
                                    if sqls is None:
                                        logs.info(f'sql语句为空，未进行前置操作')
                                    else:  # sql语句不为空
                                        ConnectMysql(database).execute_sqls(sqls)
                                        allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                                        allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                                elif type == 'api':  # 调用接口
                                    logs.info('暂不支持api的配置方式，敬请期待~~')
                                elif type is None:
                                    logs.info('前置方式为空,未进行前置操作')
                                else:
                                    logs.error(f'不支持的前置方式:{type}，未进行前置操作')
                            else:
                                logs.info('不需要前置操作')
                except Exception as e:
                    logs.error(f'前置操作失败！err={e}')
            # 流程中只有一个接口，返回列表
            else:
                try:
                    if 'setup' in testcase[1]:  # 前置操作
                        # 获取前置方式
                        type = testcase[1]['setup'].get('type', None)
                        if type == 'db':  # 数据库操作
                            database = testcase[1]['setup'].get('database', None)
                            sqls = testcase[1]['setup'].get('sqls', None)
                            if sqls is None:
                                logs.info(f'sql语句为空，未进行前置操作')
                            else:  # sql语句不为空
                                ConnectMysql(database).execute_sqls(sqls)
                                allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                                allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                        elif type == 'api':  # 调用接口
                            logs.info('暂不支持api的配置方式，敬请期待~~')
                        elif type is None:
                            logs.info('前置方式为空,未进行前置操作')
                        else:
                            logs.error(f'不支持的前置方式:{type}，未进行前置操作')
                    else:
                        logs.info('不需要前置操作')
                except Exception as e:
                    logs.error(f'前置操作失败！err={e}')

    @allure.step("测试用例后置")
    def testcase_teardown(self, testcases, index):
        """
        测试用例后置操作,从yaml 文件中获取后置方式
        :param type：填写 db 和 qpi
        :param testcases: 获取到的所有用例
        :param sqls: sql语句，支持多条
        :return:
        """
        if len(testcases) <= index:
            return
        i = 0
        for i in range(index, index + 1):
            testcase = testcases[i]
            # 判断testcase 是否为字典格式，流程中包含多个接口步骤为字典
            if isinstance(testcase, dict):
                try:
                    for key, value in testcase.items():
                        if key == 'testCase':
                            testcase = value
                            if 'teardown' in testcase[0]:
                                type = testcase[1]['teardown'].get('type', None)
                                if type == 'db':  # 数据库操作
                                    database = testcase[0]['teardown'].get('database', None)
                                    sqls = testcase[0]['teardown'].get('sqls', None)
                                    if sqls is not None:  # sql语句不为空
                                        ConnectMysql(database).execute_sqls(sqls)
                                        allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                                        allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                                elif type == 'api':  # 调用接口
                                    logs.info('暂不支持api的配置方式，敬请期待~~')
                                elif type is None:
                                    logs.error('后置方式为空,后置操作失败！')
                                else:
                                    logs.error(f'不支持的后置方式:{type}，后置操作失败！')
                            else:
                                logs.info('不需要后置操作')
                except Exception as e:
                    logs.error(f'后置操作失败！err={e}')
            # 流程中只有一个接口，返回列表
            else:
                try:
                    if 'setup' in testcase[1]:  # 前置操作
                        # 获取前置方式
                        type = testcase[1]['setup'].get('type', None)
                        if type == 'db':  # 数据库操作
                            database = testcase[1]['setup'].get('database', None)
                            sqls = testcase[1]['setup'].get('sqls', None)
                            if sqls is None:
                                logs.info(f'sql语句为空，未进行后置操作')
                            else:  # sql语句不为空
                                ConnectMysql(database).execute_sqls(sqls)
                                allure.attach(database, f"数据库名：{database}", allure.attachment_type.TEXT)
                                allure.attach(sqls, "sql语句", allure.attachment_type.TEXT)
                        elif type == 'api':  # 调用接口
                            logs.info('暂不支持api的配置方式，敬请期待~~')
                        elif type is None:
                            logs.info('后置方式为空,未进行后置操作')
                        else:
                            logs.error(f'不支持的后置方式:{type}，未进行后置操作')
                    else:
                        logs.info('不需要后置操作')
                except Exception as e:
                    logs.error(f'后置操作失败！err={e}')

    @allure.step("提取数据：正则表达式/JsonPath")
    def extract_data(self, testcase_extarct, response):
        """
        提取接口的返回值，支持正则表达式和json提取器
        :param testcase_extarct: testcase文件yaml中的extract值
        :param response: 接口的实际返回值
        :return:
        """
        try:
            pattern_lst = ['(.*?)', '(.+?)', r'(\d)', r'(\d*)']
            for key, value in testcase_extarct.items():

                # 处理正则表达式提取
                for pat in pattern_lst:
                    if pat in value:
                        ext_lst = re.search(value, response)
                        if pat in [r'(\d+)', r'(\d*)']:
                            extract_data = {key: int(ext_lst.group(1))}
                            logs.info('正则提取到的参数：%s' % extract_data)
                        else:
                            extract_data = {key: ext_lst.group(1)}
                        self.read.write_yaml_data(extract_data)

                # 处理json提取参数
                if '$' in value:
                    ext_json = jsonpath.jsonpath(json.loads(response), value)[0]
                    if ext_json:
                        extarct_data = {key: ext_json}
                        logs.info('json提取到参数：%s' % extarct_data)
                    else:
                        extarct_data = {key: '未提取到数据，请检查接口返回值是否为空！'}
                    self.read.write_yaml_data(extarct_data)
        except Exception as e:
            logs.error(e)

    @allure.step("提取数据：正则表达式/JsonPath-结果为列表")
    def extract_data_list(self, testcase_extract_list, response):
        """
        提取多个参数，支持正则表达式和json提取，提取结果以列表形式返回
        :param testcase_extract_list: yaml文件中的 extract_list信息
        :param response: 接口的实际返回值,str类型
        :return:
        """
        try:
            for key, value in testcase_extract_list.items():
                if "(.+?)" in value or "(.*?)" in value:
                    ext_list = re.findall(value, response, re.S)
                    if ext_list:
                        extract_data = {key: ext_list}
                        logs.info('正则提取到的参数：%s' % extract_data)
                        self.read.write_yaml_data(extract_data)
                if "$" in value:
                    # 增加提取判断，有些返回结果为空提取不到，给一个默认值
                    ext_json = jsonpath.jsonpath(json.loads(response), value)
                    if ext_json:
                        extract_data = {key: ext_json}
                    else:
                        extract_data = {key: "未提取到数据，该接口返回结果可能为空"}
                    logs.info('json提取到参数：%s' % extract_data)
                    self.read.write_yaml_data(extract_data)
        except:
            logs.error('接口返回值提取异常，请检查yaml文件extract_list表达式是否正确！')
